﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace FileMatcherLib
{
    public class FileMatcher
    {
        #region Nested types

        private class FileGroupComparer : IComparer<List<FileInfo>>
        {
            public static readonly FileGroupComparer Instance = new FileGroupComparer();

            public int Compare(List<FileInfo> x, List<FileInfo> y)
            {
                var fx = x[0];
                var fy = y[0];
                if (fx.Length < fy.Length)
                {
                    return -1;
                }
                if (fx.Length > fy.Length)
                {
                    return 1;
                }
                return CompareFiles(fx, fy);
            }
        }

        private class PathComparer : IComparer<DirectoryInfo>
        {
            public static readonly PathComparer Instance = new PathComparer();

            public int Compare(DirectoryInfo x, DirectoryInfo y)
            {
                return String.Compare(x.FullName, y.FullName, StringComparison.Ordinal);
            }
        }

        #endregion

        #region Constructors

        public FileMatcher(params string[] startingPaths)
            : this((IEnumerable<string>)startingPaths)
        {    
        }

        public FileMatcher(params DirectoryInfo[] stringDirs)
            : this((IEnumerable<DirectoryInfo>)stringDirs)
        {
        }

        public FileMatcher(IEnumerable<string> startingPaths)
        {
            StartingDirectories = Validate(startingPaths);
        }

        public FileMatcher(IEnumerable<DirectoryInfo> startingDirs)
        {
            StartingDirectories = Validate(startingDirs);
        }

        #endregion

        #region Properties

        public List<DirectoryInfo> StartingDirectories { get; private set; }

        #endregion

        #region Methods

        public List<List<FileInfo>> GetIdenticalFiles()
        {
            var result = new List<List<FileInfo>>();
            foreach (var sd in StartingDirectories)
            {
                AddIdenticalFiles(new FileScanner(sd), result);
            }
            for (var i = result.Count - 1; i >= 0; i--)
            {
                var fg = result[i];
                if (fg.Count < 2)
                {
                    result.RemoveAt(i);
                }
            }
            return result;
        }

        public int GetStartingSubDirectoryIndex(FileInfo file)
        {
            var dir = file.Directory;
            System.Diagnostics.Trace.Assert(dir != null);
            var pos = StartingDirectories.BinarySearch(dir, PathComparer.Instance);
            if (pos >= 0)
            {
                return pos;
            }

            pos = -pos - 1;

            pos -= 1;
            if (pos < 0)
            {
                return pos; // -1
            }

            if (IsStrictSubStringIgnoreCase(StartingDirectories[pos].FullName, dir.FullName))
            {
                return pos;
            }
            return -1;
        }

        protected static bool IsStrictSubStringIgnoreCase(string candidateSubstr, string superStr)
        {
            if (candidateSubstr.Length >= superStr.Length)
            {
                return false;
            }
            return String.Compare(candidateSubstr, superStr.Substring(0, candidateSubstr.Length),
                StringComparison.OrdinalIgnoreCase) == 0;
        }


        protected void AddIdenticalFiles(FileScanner fscan, List<List<FileInfo>> ifgs)
        {
            for (var i = 0; i < fscan.FilesBySize.Count; )
            {
                var f = fscan.FilesBySize[i];
                var len = f.Length;
                var iStart = i;
                do
                {
                    i++;
                } while (i < fscan.FilesBySize.Count && fscan.FilesBySize[i].Length == len);

                AddIdenticalGroups(fscan.FilesBySize.GetRange(iStart, i - iStart), ifgs);
            }
        }

        /// <summary>
        ///  Groups identical files from a collection of files with the same size
        /// </summary>
        /// <param name="sameSizeGroup">The collection of the files with the same size to group from</param>
        /// <returns>
        ///  A list of groups of identical files, all files with the same size, sorted in the order defined by 
        ///  FileGroupComparer with Method CompareFiles() (size in ascending order first and byte sequence in 
        ///  ascending dictonary order)
        /// </returns>
        protected List<List<FileInfo>> GroupIdentical(IList<FileInfo> sameSizeGroup)
        {
            var result = new List<List<FileInfo>>();
            foreach (var f in sameSizeGroup)
            {
                var fgff = new List<FileInfo> { f };
                var idx = result.BinarySearch(fgff, FileGroupComparer.Instance);
                if (idx < 0)
                {
                    idx = -idx - 1;
                    result.Insert(idx, fgff);
                }
                else
                {
                    result[idx].Add(f);
                }
            }
            return result;
        }

        /// <summary>
        ///  Groups identical files from the same size group first into identical groups and adds them to the specified
        ///  identicalGroup in the order specified by FileGroupComparer (size in ascending order first and then byte
        ///  seuqence in ascending dictionary order)
        /// </summary>
        /// <param name="sameSizeGroup">A colllection of files with the same size to be grouped and added to identical groups</param>
        /// <param name="identicalGroups">A list of groups of identical files</param>
        protected void AddIdenticalGroups(IList<FileInfo> sameSizeGroup, List<List<FileInfo>> identicalGroups)
        {
            var tempFileGrps = GroupIdentical(sameSizeGroup);
            foreach (var fg in tempFileGrps)
            {
                var idx = identicalGroups.BinarySearch(fg, FileGroupComparer.Instance);
                if (idx < 0)
                {
                    idx = -idx - 1;
                    identicalGroups.Insert(idx, fg);
                }
                else
                {
                    var ig = identicalGroups[idx];
                    ig.AddRange(fg);
                }
            }
        }

        /// <summary>
        ///  Returns how two files (with the same length assumedly) compare
        /// </summary>
        /// <param name="f1">The 1st file to compare</param>
        /// <param name="f2">The 2nd file to compare</param>
        /// <returns>An integer indicating the result of the comnparison</returns>
        protected static int CompareFiles(FileInfo f1, FileInfo f2)
        {
            var len = f1.Length;
            var fs1 = f1.OpenRead();
            var fs2 = f2.OpenRead();
            const int bufSize = 4096;
            var buf1 = new byte[bufSize];
            var buf2 = new byte[bufSize];

            for (var totalRead = 0; totalRead < len; )
            {
                var read1 = fs1.Read(buf1, 0, bufSize);
                var read2 = fs2.Read(buf2, 0, bufSize);
                System.Diagnostics.Trace.Assert(read1 == read2);
                totalRead += read1;
                for (var i = 0; i < read1; i++)
                {
                    var b1 = buf1[i];
                    var b2 = buf2[i];
                    if (b1 == b2)
                    {
                        continue;
                    }
                    if (b1 < b2)
                    {
                        return -1;
                    }
                    if (b1 > b2)
                    {
                        return 1;
                    }
                }
            }
            return 0;
        }

        protected List<DirectoryInfo> Validate(IEnumerable<DirectoryInfo> startingDirs)
        {
            var result = new List<DirectoryInfo>();
         
            foreach (var sp in startingDirs)
            {
                var pos = result.BinarySearch(sp, PathComparer.Instance);
                if (pos >= 0)
                {
                    continue;
                }
                pos = -pos - 1;

                if (pos < result.Count && IsStrictSubStringIgnoreCase(sp.FullName, result[pos].FullName))
                {
                    var j = pos;
                    do
                    {
                        j++;
                    } while (j < result.Count && IsStrictSubStringIgnoreCase(sp.FullName, result[j].FullName));
                    result.RemoveRange(pos, j - pos);
                    result.Insert(pos, sp);
                }
                else if (pos > 0 && IsStrictSubStringIgnoreCase(result[pos - 1].FullName, sp.FullName))
                {

                    result.RemoveAt(pos - 1);
                    result.Insert(pos - 1, sp);
                }
                else
                {
                    result.Insert(pos, sp);
                }
            }

            return result;
        }

        protected List<DirectoryInfo> Validate(IEnumerable<string> startingPaths)
        {
            var startingDirs = startingPaths.Select(sp => new DirectoryInfo(sp)).ToList();
            return Validate(startingDirs);
        }

        #endregion
    }
}
